//
// Created by tianq on 25-5-3.
// Later we want one more type of shape as triangles and rectangles but we no longer squares and circles.
//

#include <cmath>
#include <SFML/Graphics.hpp>
#include <windows.h>
#include <commdlg.h>
#include <fstream>

using namespace std;

// count intersection of given edge and line strip
int countIntersection(const sf::VertexArray &lineStrip, const sf::Vector2f &p1, const sf::Vector2f &p2) {
    int count = 0;

    // Iterate through all line segments in the line strip
    for (int i = 0; i < lineStrip.getVertexCount() - 1; ++i) {
        // Get the current segment's endpoints
        const auto &segP1 = lineStrip[i].position;
        const auto &segP2 = lineStrip[i + 1].position;

        // Calculate intersection using standard line-line intersection formula
        float denominator = (segP2.y - segP1.y) * (p2.x - p1.x) - (segP2.x - segP1.x) * (p2.y - p1.y);

        // If lines are parallel, skip
        if (std::abs(denominator) < 1e-6)
            continue;

        // Calculate intersection parameters
        const float ua =
                ((segP2.x - segP1.x) * (p1.y - segP1.y) - (segP2.y - segP1.y) * (p1.x - segP1.x)) / denominator;
        const float ub =
                ((p2.x - p1.x) * (p1.y - segP1.y) - (p2.y - p1.y) * (p1.x - segP1.x)) / denominator;

        // Check if intersection is within both line segments
        if (ua > 0.0f && ua < 1.0f && ub > 0.0f && ub < 1.0f) {
            count++;
        }
    }

    return count;
}

void addVertex(sf::VertexArray &lineStrip, int &intersectionCount, const sf::Vector2f &pos) {
    const auto v = sf::Vertex(pos);
    if (lineStrip.getVertexCount() >= 3) {
        // count intersection
        intersectionCount -= countIntersection(
            lineStrip,
            lineStrip[lineStrip.getVertexCount() - 2].position,
            lineStrip[lineStrip.getVertexCount() - 1].position);
        // pop the last one(starting point), and append a new one
        lineStrip[lineStrip.getVertexCount() - 1] = v;
        lineStrip.append(lineStrip[0]);
        // add 2 new line segments, update intersection count
        intersectionCount += countIntersection(
            lineStrip,
            lineStrip[lineStrip.getVertexCount() - 2].position,
            lineStrip[lineStrip.getVertexCount() - 1].position);
        intersectionCount += countIntersection(
            lineStrip,
            lineStrip[lineStrip.getVertexCount() - 3].position,
            lineStrip[lineStrip.getVertexCount() - 2].position);
    } else if (lineStrip.getVertexCount() == 2) {
        // make a loop, a triangle can't have any intersections
        lineStrip.append(v);
        lineStrip.append(lineStrip[0]);
    } else {
        lineStrip.append(v);
    }
}

void moveVertex(sf::VertexArray &lineStrip, int &intersectionCount, const sf::Vector2f &pos, int pointIndex = -1) {
    if (pointIndex == -1) {
        // by default, move last inserted point
        if (lineStrip.getVertexCount() >= 3)
            pointIndex = static_cast<int>(lineStrip.getVertexCount()) - 2; // we repeated the first point
        else
            pointIndex = static_cast<int>(lineStrip.getVertexCount()) - 1;
    }

    if (lineStrip.getVertexCount() < 3) {
        lineStrip[pointIndex].position = pos;
        return;
    }

    // real vertex count, removing the last (repeating) point
    const int v = static_cast<int>(lineStrip.getVertexCount()) - 1;

    // remove 2 line segments, update intersection count
    intersectionCount -= countIntersection(
        lineStrip,
        lineStrip[pointIndex].position,
        lineStrip[(pointIndex - 1 + v) % v].position);
    intersectionCount -= countIntersection(
        lineStrip,
        lineStrip[pointIndex].position,
        lineStrip[(pointIndex + 1 + v) % v].position);

    // move the point
    lineStrip[pointIndex].position = pos;
    // the first point is repeated, move both
    if (pointIndex == 0)
        lineStrip[v].position = pos;

    // add 2 new line segments, update intersection count
    intersectionCount += countIntersection(
        lineStrip,
        lineStrip[pointIndex].position,
        lineStrip[(pointIndex - 1 + v) % v].position);
    intersectionCount += countIntersection(
        lineStrip,
        lineStrip[pointIndex].position,
        lineStrip[(pointIndex + 1 + v) % v].position);
}

void deleteLastVertex(sf::VertexArray &lineStrip, int &intersectionCount) {
    // considering the implementation of sf::VertexArray, we can only delete the last point
    if (lineStrip.getVertexCount() > 4) {
        // update intersection count
        intersectionCount -= countIntersection(
            lineStrip,
            lineStrip[lineStrip.getVertexCount() - 2].position,
            lineStrip[lineStrip.getVertexCount() - 1].position);
        intersectionCount -= countIntersection(
            lineStrip,
            lineStrip[lineStrip.getVertexCount() - 3].position,
            lineStrip[lineStrip.getVertexCount() - 2].position);

        lineStrip.resize(lineStrip.getVertexCount() - 1);
        lineStrip[lineStrip.getVertexCount() - 1] = lineStrip[0];

        intersectionCount += countIntersection(
            lineStrip,
            lineStrip[lineStrip.getVertexCount() - 2].position,
            lineStrip[lineStrip.getVertexCount() - 1].position);
    } else if (lineStrip.getVertexCount() == 4) { // 4=3+1 repeated first point
        // remove the repeated first point and the last point
        lineStrip.resize(lineStrip.getVertexCount() - 2);
    } else if (lineStrip.getVertexCount() > 0) {
        lineStrip.resize(lineStrip.getVertexCount() - 1);
    }
}

pair<double, double> calculateAreaAndPerimeter(sf::VertexArray &lineStrip) {
    if (lineStrip.getVertexCount() < 3)
        return {0.0, 0.0};
    double area = 0.0;
    double peri = 0.0;
    for (int i = 0; i < lineStrip.getVertexCount() - 1; i++) {
        const auto &p1 = lineStrip[i];
        const auto &p2 = lineStrip[i + 1];
        // shoelace formula https://artofproblemsolving.com/wiki/index.php?title=Shoelace_Theorem#External_Links
        area += (p1.position.x * p2.position.y) - (p2.position.x * p1.position.y);

        // this one is also shoelace, but slightly less robust
        // area += (p2.position.x - p1.position.x) * (p2.position.y * p1.position.y);

        peri += (p1.position - p2.position).length();
    }
    area = abs(area) / 2;
    return {area, peri};
}


void saveFileDialog(sf::VertexArray &lineStrip, string &str, const sf::RenderWindow &window) {
    // 保存处理
    OPENFILENAMEA ofn;
    char szFile[260] = {0};
    ZeroMemory(&ofn, sizeof(ofn));
    ofn.lStructSize = sizeof(ofn);
    ofn.hwndOwner = window.getNativeHandle();
    ofn.lpstrFile = szFile;
    ofn.nMaxFile = sizeof(szFile);
    ofn.lpstrFilter = "CSV Files\0*.csv\0";
    ofn.nFilterIndex = 1;
    ofn.Flags = OFN_PATHMUSTEXIST | OFN_OVERWRITEPROMPT;

    if (GetSaveFileNameA(&ofn)) {
        ofstream fout(ofn.lpstrFile);
        // -1: remove repeated point
        for (int i = 0; i < lineStrip.getVertexCount() - 1; i++) {
            auto &p = lineStrip[i].position;
            fout << p.x << "," << p.y << "\n";
        }
        str += "Saved " + to_string(lineStrip.getVertexCount()) + " points\n";
    }
}

void loadFileDialog(sf::VertexArray &lineStrip, int &intersectionCount, string &str, const sf::RenderWindow &window) {
    // 加载处理
    OPENFILENAMEA ofn;
    char szFile[260] = {0};
    ZeroMemory(&ofn, sizeof(ofn));
    ofn.lStructSize = sizeof(ofn);
    ofn.hwndOwner = window.getNativeHandle();
    ofn.lpstrFile = szFile;
    ofn.nMaxFile = sizeof(szFile);
    ofn.lpstrFilter = "CSV Files\0*.csv\0";
    ofn.nFilterIndex = 1;
    ofn.Flags = OFN_PATHMUSTEXIST | OFN_FILEMUSTEXIST;

    if (GetOpenFileNameA(&ofn)) {
        lineStrip.clear();
        intersectionCount = 0;
        ifstream fin(ofn.lpstrFile);
        string line;
        while (getline(fin, line)) {
            if (const size_t comma = line.find(','); comma != string::npos) {
                float x = stof(line.substr(0, comma));
                float y = stof(line.substr(comma + 1));
                addVertex(lineStrip, intersectionCount, {x, y});
            }
        }
        str += "Loaded " + to_string(lineStrip.getVertexCount()) + " points\n";
    }
}

template<class T>
string toAlignedString(T in, const int width = 4) {
    string str = to_string(in);
    if (str.length() < width) {
        str.insert(0, width - str.length(), ' ');
    } else if (str.length() > width) {
        str = str.substr(0, width - 1) + "*";
    }
    return str;
}

int main() {
    sf::RenderWindow window(sf::VideoMode({800, 600}), "SFML polygon");

    window.setMaximumSize(sf::Vector2u{800, 600});
    window.setMinimumSize(sf::Vector2u{800, 600});

    // draw a line strip, representing input shape
    // since it maybe a concave, we are not filling it, hence using a line strip here
    sf::VertexArray lineStrip(sf::PrimitiveType::LineStrip);
    // vector<sf::Vector2f> vertices; // aka points. this is an ordered list

    // count of intersections, when value non-zero, skip area calculation
    int intersectionCount = 0;

    // point indicator
    // keep in mind that pos is at top left, not center
    constexpr float radius = 10;
    sf::CircleShape point(radius);
    bool showPoint = true;

    // sf::ConvexShape mouse;
    // mouse.setPointCount(3);
    // mouse.setPoint(0, sf::Vector2f(0, 0));
    // mouse.setPoint(1, sf::Vector2f(20 * sqrtf(2), 20 * sqrtf(2)));
    // mouse.setPoint(2, sf::Vector2f(0, 40));

    sf::RectangleShape hLine({800, 1}), vLine({1, 600});
    // don't forget tooltip text

    // we draw our own cursor, so hide the default one
    // window.setMouseCursorVisible(false); // set after welcome
    bool useCustomMouse = true;

    // if using vsync, do PollEvent instead of Wait Event
    // we may emit too much frames to draw, resulting in render queuing and a huge lag
    // window.setVerticalSyncEnabled(true);

    // handle mouse button
    bool leftDown = false;
    bool rightDown = false;

    // on click and drag, xxDown=false, xxHold=true
    bool leftHold = false;
    bool rightHold = false;
    sf::Vector2i mousePos;

    // index of the point editing
    int pointIndex = -1; // -1 means no point is editing

    string str;
    // add startup message
    str += "Polygon area calculator\n\n";
    str += "Left Click: add a point\n";
    str += "Right Click: move nearest point\n\n";
    str += "Backspace: remove last point\n";
    str += "Ctrl+S: save as csv\n";
    str += "Ctrl+L: load from csv\n";
    str += "M: toggle mouse style\n";
    str += "P: toggle point indicator\n\n";
    str += "click to start";

    // I hope this doesn't bring copyright issues
    const sf::Font font("resources/hei.ttf");
    // Create a text
    sf::Text text(font, str);
    text.setCharacterSize(30);
    text.setStyle(sf::Text::Bold);
    text.setFillColor(sf::Color::White);

    sf::Text tooltip(font, "x:0\ny:0");
    tooltip.setCharacterSize(10);
    tooltip.setFillColor(sf::Color::White);

    // show welcome message, wait for click
    window.draw(text);
    window.display();
    while (const std::optional event = window.waitEvent()) {
        if (event->is<sf::Event::MouseButtonPressed>()) break;
        if (event->is<sf::Event::Closed>()) exit(0);
    }
    window.setMouseCursorVisible(false);

    // performance metric
    sf::Clock clock;

    while (true) {
        sf::Time logicTime = clock.getElapsedTime();

        window.clear();

        if (showPoint) {
            point.setFillColor(sf::Color::Yellow);
            for (int i = 1; i < lineStrip.getVertexCount(); i++) {
                point.setPosition(lineStrip[i].position - sf::Vector2f(radius, radius));
                window.draw(point);
            }
            if (lineStrip.getVertexCount() > 0) {
                point.setFillColor(sf::Color::Red);
                point.setPosition(lineStrip[0].position - sf::Vector2f(radius, radius));
                window.draw(point);
            }
        }

        window.draw(lineStrip);

        // Draw text (frame time, mouse pos, etc)
        text.setString(str);
        window.draw(text);

        if (useCustomMouse) {
            // Draw mouse, now that it can't be covered
            // window.draw(mouse);
            window.draw(vLine);
            window.draw(hLine);
            window.draw(tooltip);
        }

        window.display();

        // Performance metric
        sf::Time totalTime = clock.getElapsedTime();
        sf::Time drawTime = totalTime - logicTime; // render time of the last frame, not current frame

        str = "total: " + toAlignedString(totalTime.asMicroseconds()) + "us, "
              + "logic: " + toAlignedString(logicTime.asMicroseconds()) + "us, "
              + "draw: " + toAlignedString(drawTime.asMicroseconds()) + "us\n";
        // since we only draw on event arrival, there's no such thing as FPS
        // str += "FPS: " + std::to_string(1000000 / totalTime.asMicroseconds()) + "\n";

        clock.stop(); // waiting for input

        // stop and wait for user input
        // this is intentional, for power saving
        const std::optional event = window.waitEvent();

        // time the logic process
        clock.restart();

        if (event->is<sf::Event::Closed>()) {
            window.close();
            // exit(0);
            break;
        }

        // handle keyboard
        // backspace: delete last point
        if (event->is<sf::Event::KeyPressed>()) {
            if (event->getIf<sf::Event::KeyPressed>()->code == sf::Keyboard::Key::Backspace) {
                deleteLastVertex(lineStrip, intersectionCount);
            } else if (
                event->getIf<sf::Event::KeyPressed>()->code == sf::Keyboard::Key::P
            ) {
                showPoint = !showPoint;
            } else if (
                event->getIf<sf::Event::KeyPressed>()->code == sf::Keyboard::Key::M
            ) {
                useCustomMouse = !useCustomMouse;
                window.setMouseCursorVisible(!useCustomMouse);
            } else if (
                event->getIf<sf::Event::KeyPressed>()->code == sf::Keyboard::Key::S &&
                event->getIf<sf::Event::KeyPressed>()->control
            ) {
                saveFileDialog(lineStrip, str, window);
            } else if (
                event->getIf<sf::Event::KeyPressed>()->code == sf::Keyboard::Key::L &&
                event->getIf<sf::Event::KeyPressed>()->control
            ) {
                loadFileDialog(lineStrip, intersectionCount, str, window);
            }
        }

        if (event->is<sf::Event::MouseMoved>()) {
            mousePos = event->getIf<sf::Event::MouseMoved>()->position;
        }

        if (event->is<sf::Event::MouseButtonPressed>()) {
            mousePos = event->getIf<sf::Event::MouseButtonPressed>()->position;
            if (event->getIf<sf::Event::MouseButtonPressed>()->button == sf::Mouse::Button::Left)
                leftDown = leftHold = true;
            if (event->getIf<sf::Event::MouseButtonPressed>()->button == sf::Mouse::Button::Right)
                rightDown = rightHold = true;
        }

        if (event->is<sf::Event::MouseButtonReleased>()) {
            mousePos = event->getIf<sf::Event::MouseButtonReleased>()->position;
            if (event->getIf<sf::Event::MouseButtonReleased>()->button == sf::Mouse::Button::Left)
                leftDown = leftHold = false;
            if (event->getIf<sf::Event::MouseButtonReleased>()->button == sf::Mouse::Button::Right)
                rightDown = rightHold = false;
        }

        const auto mousePosF = sf::Vector2f(mousePos);
        // mouse.setPosition(mousePosF);
        vLine.setPosition({mousePosF.x, 0});
        hLine.setPosition({0, mousePosF.y});
        tooltip.setPosition(mousePosF + sf::Vector2f{10, 10});
        tooltip.setString("x:" + std::to_string(mousePos.x) + "\ny:" + std::to_string(mousePos.y));
        // str += "MousePos: (" + std::to_string(mousePos.x) + ", " + std::to_string(mousePos.y) + ")\n";

        // handle graphic edit
        if (leftDown) {
            addVertex(lineStrip, intersectionCount, mousePosF);
            leftDown = false;
        }

        if (leftHold) {
            moveVertex(lineStrip, intersectionCount, mousePosF);
            // continue; // skip right click handling
        }

        // right click: move nearest point by dragging
        if (rightDown) {
            float minDist = numeric_limits<float>::max();
            for (int i = 0; i < lineStrip.getVertexCount(); i++) {
                if (
                    const float dist = (lineStrip[i].position - mousePosF).length();
                    dist < minDist
                ) {
                    minDist = dist;
                    pointIndex = i;
                }
            }
            rightDown = false;
        }

        if (rightHold) {
            moveVertex(lineStrip, intersectionCount, mousePosF, pointIndex);
            // //debug
            // str += "pointIndex: " + std::to_string(pointIndex) + "\n";
            // str += "left:  " + std::to_string((pointIndex - 1) % (lineStrip.getVertexCount() - 1)) + "\n";
            // str += "right: " + std::to_string((pointIndex + 1) % (lineStrip.getVertexCount() - 1)) + "\n";
            // str += "left fix:  " + std::to_string(
            //     (pointIndex - 1 + lineStrip.getVertexCount() - 1) % (lineStrip.getVertexCount() - 1)) + "\n";
            // str += "right fix: " + std::to_string(
            //     (pointIndex + 1 + lineStrip.getVertexCount() - 1) % (lineStrip.getVertexCount() - 1)) + "\n";
        }

        // we need at least 3 points to calculate area
        if (lineStrip.getVertexCount() < 3) {
            str += to_string(lineStrip.getVertexCount()) + " points, calculation needs 3 or more\n";
            continue;
        }

        // Skip area and perimeter calculation if there exists intersection
        if (intersectionCount > 0) {
            str += toAlignedString(intersectionCount, 2) + " intersections, calculation skipped\n";
            continue;
        }

        auto [area,peri] = calculateAreaAndPerimeter(lineStrip);
        str += "area = " + std::to_string(area) + ", peri = " + std::to_string(peri) + "\n";
    }
}
